﻿namespace DistrRTgen
{
    using rtgen_worker;
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading;
    using System.Xml;

    public class ChainGenerator : MarshalByRefObject
    {
        private ChainCheckHashList _CCList;
        private DistrRTgenConnector _Connection;
        private long _CurrentSpeed;
        private IndexHashList _IHList;
        private Thread _MainThread;
        private int _MaxProcessorsToUse;
        private RainbowCrack _RainbowCrack;
        private System.Threading.Timer _RequestWorkTimer;
        private WorkThread[] _WorkThreads;
        private WorkUnitList _WUList;
        private static int numInstances;

        public event ucCurrentSpeedChanged onCurrentSpeedChanged;

        public ChainGenerator()
        {
            if (++numInstances > 1)
            {
                Logger.AddLogLine("WARNING: Multiple instances of ChainGenerator has been started", LoggerLevel.LOG_ERROR);
            }
            Settings.Load();
            this._MaxProcessorsToUse = Settings.NumCPUToUse;
            this._WorkThreads = new WorkThread[this._MaxProcessorsToUse];
            this._WUList = WorkUnitList.LoadWUList();
            this._WUList.SaveEnabled = true;
            this._IHList = new IndexHashList();
            this._CCList = new ChainCheckHashList();
            this._RainbowCrack = new RainbowCrack();
        }

        private void _Process_Exited(object sender, EventArgs e)
        {
            Process Process = (Process) sender;
            WUProcess WUP = this.GetProcess(Process.Id);
            if (WUP != null)
            {
                if (Process.ExitCode != 0)
                {
                    Logger.AddLogLine(string.Concat(new object[] { "Process ", Process.Id, " terminated with ExitCode ", Process.ExitCode, " for part ", WUP.PartID }), LoggerLevel.LOG_INFO);
                    if (WUP.NumTimesStarted < 5)
                    {
                        WUP.Restart();
                    }
                    else
                    {
                        Logger.AddLogLine(string.Concat(new object[] { "Process for part ", WUP.PartID, " keeps dying. Part will be removed. Info:\nStdOut: ", WUP.StdOut.ReadToEnd(), "\nStdErr:", WUP.StdErr.ReadToEnd() }), LoggerLevel.LOG_ERROR);
                        this._WUList.Remove(WUP.PartID);
                        int Index = this.GetProcessIndex(Process.Id);
                        if (Index >= 0)
                        {
                            this.StartProcess(Index);
                        }
                        else
                        {
                            Logger.AddLogLine("Process " + Process.Id + " not found in process stack. Ignoring", LoggerLevel.LOG_DEBUG);
                        }
                    }
                }
                else
                {
                    int IsDone = WUP.IsDone();
                    if (IsDone == 1)
                    {
                        WUP.Work.BeginUploading();
                        WUP.Work.Done = true;
                        Logger.AddLogLine("Part " + WUP.PartID + " finished. Upload started", LoggerLevel.LOG_INFO);
                        int returnCode = RainbowCrack.SortFile(WUP.PartID + ".part");
                        Logger.AddLogLine("Sortfile returned " + returnCode, LoggerLevel.LOG_DEBUG);
                        Thread.Sleep(5000);
                        if (!this._Connection.UploadFinishedPart(WUP.Work, new UploadFileCompletedEventHandler(this.UploadFileCompleted), new UploadProgressChangedEventHandler(this.UploadProgressChanged)))
                        {
                            WUP.Work.EndUploading();
                        }
                        int Index = this.GetProcessIndex(Process.Id);
                        if (Index >= 0)
                        {
                            this.StartProcess(Index);
                        }
                    }
                    else
                    {
                        Logger.AddLogLine(string.Concat(new object[] { "Process ", WUP.PartID, " exited with code ", IsDone }), LoggerLevel.LOG_ALERT);
                        if (WUP.NumTimesStarted < 5)
                        {
                            Logger.AddLogLine("Process for part " + WUP.PartID + " died. Trying to restart", LoggerLevel.LOG_ALERT);
                            WUP.Restart();
                        }
                        else
                        {
                            Logger.AddLogLine(string.Concat(new object[] { "Process for part ", WUP.PartID, " keeps dying. Part will be removed. Info:\nStdOut: ", WUP.StdOut.ReadToEnd(), "\nStdErr:", WUP.StdErr.ReadToEnd() }), LoggerLevel.LOG_ERROR);
                            this._WUList.Remove(WUP.PartID);
                            int Index = this.GetProcessIndex(Process.Id);
                            if (Index >= 0)
                            {
                                this.StartProcess(Index);
                            }
                            else
                            {
                                Logger.AddLogLine("Process " + Process.Id + " not found in process stack. Ignoring", LoggerLevel.LOG_DEBUG);
                            }
                        }
                    }
                }
            }
        }

        private void _Thread_Exited(object sender, EventArgs e)
        {
            if (sender.GetType() == typeof(ChainCheckHashThread))
            {
                ChainCheckHashThread thread = (ChainCheckHashThread) sender;
                if (thread.Work.ErrorCode == 0)
                {
                    this._Connection.SendChainCheckResult(thread.Work.RequestID, thread.Work.Password);
                }
                this._CCList.Remove(thread.Work);
                this.StartProcess(thread);
            }
            else if (sender.GetType() == typeof(HashIndexerThread))
            {
                HashIndexerThread thread = (HashIndexerThread) sender;
                this._Connection.UploadHashIndexList(thread.Work);
                this._IHList.Remove(thread.Work);
                this.StartProcess(thread);
            }
            else
            {
                Logger.AddLogLine("Unknown thread exited: " + sender.GetType(), LoggerLevel.LOG_ERROR);
            }
        }

        public void AsyncStart()
        {
            if ((this._MainThread == null) || (this._MainThread.ThreadState != System.Threading.ThreadState.Running))
            {
                this._MainThread = new Thread(new ThreadStart(this.Start));
                this._MainThread.Start();
            }
        }

        public WUProcess GetProcess(int ProcessID)
        {
            for (int i = 0; i < this._WorkThreads.Length; i++)
            {
                if ((this._WorkThreads[i] != null) && (this._WorkThreads[i].GetType() == typeof(WUProcess)))
                {
                    WUProcess proc = (WUProcess) this._WorkThreads[i];
                    if (proc.ProcessID == ProcessID)
                    {
                        return proc;
                    }
                }
            }
            return null;
        }

        public int GetProcessIndex(int ProcessID)
        {
            for (int i = 0; i < this._WorkThreads.Length; i++)
            {
                if ((this._WorkThreads[i] != null) & (this._WorkThreads[i].GetType() == typeof(WUProcess)))
                {
                    WUProcess proc = (WUProcess) this._WorkThreads[i];
                    if (proc.ProcessID == ProcessID)
                    {
                        return i;
                    }
                }
            }
            return -1;
        }

        public int RegisterClient()
        {
            return this._Connection.RegisterClient();
        }

        public void RequestWorkTimerCallback(object StateInfo)
        {
            this._RequestWorkTimer = null;
        }

        public void Start()
        {
            try
            {
                if ((Settings.Username == "") || (Settings.Password == ""))
                {
                    Logger.AddLogLine("No username or password is set. ChainGenerator will not start before credentials are given", LoggerLevel.LOG_ALERT);
                }
                else
                {
                    Logger.AddLogLine("Starting ChainGenerator", LoggerLevel.LOG_DEBUG);
                    Logger.AddLogLine(Environment.ProcessorCount + " processors found", LoggerLevel.LOG_INFO);
                    this._Connection = new DistrRTgenConnector();
                    if (!this._Connection.UpdateCharset("charset.txt") && File.Exists("charset.txt") == false)
                    {
                        Logger.AddLogLine("Unable to download charset.txt. ChainGenerator will not start.\nCheck if http://distributed.freerainbowtables.com/charset.txt is available from the machine", LoggerLevel.LOG_ALERT);
                    }
                    else
                    {
                        long[] ChainProgressThisRound = new long[this._WorkThreads.Length];
                        long TotalChainsDonePrevRound = 0L;
                        DateTime LastRound = DateTime.Now;
                        while (true)
                        {
                            DateTime Now = DateTime.Now;
                            for (int i = 0; i < this._WorkThreads.Length; i++)
                            {
                                if ((this._WorkThreads[i] != null) && (this._WorkThreads[i].GetType() == typeof(WUProcess)))
                                {
                                    ChainProgressThisRound[i] = ((WUProcess) this._WorkThreads[i]).GetNumChainsDone();
                                    if ((Settings.WorkStartTime.TimeOfDay > DateTime.Now.TimeOfDay) || (Settings.WorkEndTime.TimeOfDay < DateTime.Now.TimeOfDay))
                                    {
                                        Logger.AddLogLine("Stopping WorkThread due to out of generation timezone", LoggerLevel.LOG_DEBUG);
                                        this._WorkThreads[i].Stop();
                                        this._WorkThreads[i] = null;
                                    }
                                }
                                if (((this._WorkThreads[i] == null) && (Settings.WorkStartTime.TimeOfDay < DateTime.Now.TimeOfDay)) && (Settings.WorkEndTime.TimeOfDay > DateTime.Now.TimeOfDay))
                                {
                                    this.StartProcess(i);
                                }
                            }
                            TimeSpan diff = (TimeSpan) (Now - LastRound);
                            LastRound = Now;
                            long TotalChainsDoneNow = 0L;
                            for (int i = 0; i < ChainProgressThisRound.Length; i++)
                            {
                                TotalChainsDoneNow += ChainProgressThisRound[i];
                            }
                            double NumChainsDone = TotalChainsDoneNow - TotalChainsDonePrevRound;
                            double diffTime = diff.TotalMilliseconds / 1000.0;
                            this.CurrentSpeed = (long) (NumChainsDone / diffTime);
                            TotalChainsDonePrevRound = TotalChainsDoneNow;
                            for (int i = 0; i < this._WUList.Count; i++)
                            {
                                if ((this._WUList[i].Done && !this._WUList[i].Uploading) && (this._WUList[i].LastUploadTry <= (DateTime.Now - TimeSpan.FromMinutes(10.0))))
                                {
                                    Logger.AddLogLine("Restarting upload of part " + this._WUList[i].PartID, LoggerLevel.LOG_DEBUG);                                    
                                    this._Connection.UploadFinishedPart(_WUList[i], new UploadFileCompletedEventHandler(this.UploadFileCompleted), new UploadProgressChangedEventHandler(this.UploadProgressChanged));
                                }
                            }
                            Thread.Sleep(0x2710);
                        }
                    }
                }
            }
            catch (ThreadAbortException)
            {
                for (int i = 0; i < this._WorkThreads.Length; i++)
                {
                    if (this._WorkThreads[i] != null)
                    {
                        this._WorkThreads[i].Stop();
                        this._WorkThreads[i] = null;
                    }
                }
            }
        }

        private void StartProcess(WorkThread WT)
        {
            for (int i = 0; i < this._WorkThreads.Length; i++)
            {
                if (this._WorkThreads[i] == WT)
                {
                    this.StartProcess(i);
                }
            }
        }

        private void StartProcess(int Index)
        {
            bool DoCracking = true;
            for (int i = 0; i < this._WorkThreads.Length; i++)
            {
                if (((i != Index) && (this._WorkThreads[i] != null)) && ((this._WorkThreads[i].GetType() == typeof(HashIndexerThread)) || (this._WorkThreads[i].GetType() == typeof(ChainCheckHashThread))))
                {
                    DoCracking = false;
                }
            }
            if (DoCracking)
            {
                ChainCheckHash CC = _CCList.GetFirstAvailable();
                if (CC != null)
                {
                    this._WorkThreads[Index] = new ChainCheckHashThread(CC, new EventHandler(this._Thread_Exited));
                    this._WorkThreads[Index].Start();
                    return;
                }
                IndexHash IH = this._IHList.GetFirstAvailable();
                if (IH != null)
                {
                    this._WorkThreads[Index] = new HashIndexerThread(IH, new EventHandler(this._Thread_Exited));
                    this._WorkThreads[Index].Start();
                    return;
                }
            }
            WorkUnit WU = this._WUList.GetFirstAvailable(_MaxProcessorsToUse * 4);
            if (WU == null)
            {
                Logger.AddLogLine("No more work left in queue, and unable to obtain more work from server. Trying again later", LoggerLevel.LOG_INFO);
                this._WorkThreads[Index] = null;
                this.StartRequestTimer();
            }
            else
            {
                this._WorkThreads[Index] = new WUProcess(WU, new EventHandler(this._Process_Exited));
                this._WorkThreads[Index].Start();
            }
        }

        private void StartRequestTimer()
        {
            if (this._RequestWorkTimer != null)
            {
                TimerCallback timerDelegate = new TimerCallback(this.RequestWorkTimerCallback);
                this._RequestWorkTimer = new System.Threading.Timer(timerDelegate, null, TimeSpan.FromMinutes(10.0), TimeSpan.FromDays(1000.0));
            }
        }

        public void Stop()
        {
            Logger.AddLogLine("Stopping ChainGenerator", LoggerLevel.LOG_ALERT);
            if (this._MainThread != null)
            {
                this._MainThread.Abort();
                while (this._MainThread.IsAlive)
                {
                }
                this._MainThread = null;
            }
        }

        private void UploadFileCompleted(object sender, UploadFileCompletedEventArgs e)
        {
            uint PartID = (uint) e.UserState;
            if (e.Cancelled)
            {
                Logger.AddLogLine(string.Concat(new object[] { "Upload of part ", PartID, " was cancelled. Reason: ", e.Error.Message }), LoggerLevel.LOG_ALERT);
                WorkUnit WU = this.WUList.GetWUByPartID(PartID);
                if (WU != null)
                {
                    WU.EndUploading();
                }
                return;
            }
            if (e.Error != null)
            {
                Logger.AddLogLine("Error while uploading part " + PartID + ": " + e.Error.Message, LoggerLevel.LOG_INFO);
                WorkUnit WU = this.WUList.GetWUByPartID(PartID);
                if (WU != null)
                {
                    WU.EndUploading();
                }
                return;
            }
            XmlDocument xmlResult = new XmlDocument();
            string sResult = Encoding.ASCII.GetString(e.Result);
            try
            {
                xmlResult.LoadXml(sResult);
            }
            catch (Exception ex)
            {
                Logger.AddLogLine("Invalid response recieved from server. Upload of part " + PartID + " aborted: '" + sResult + "'", LoggerLevel.LOG_INFO);
                WorkUnit WU = this.WUList.GetWUByPartID(PartID);
                if(WU != null)
                    WU.EndUploading();               
                return;
            }
            XmlElement oRoot = xmlResult.DocumentElement;
            for (int i = 0; i < oRoot.ChildNodes.Count; i++)
            {
                if (oRoot.ChildNodes[i].Name == "error")
                {
                    if (oRoot.ChildNodes[i].Attributes["code"].Value == "300")
                    {
                        Logger.AddLogLine("Part" + PartID + " was rejected by the server. Bad compression", LoggerLevel.LOG_ALERT);
                        this._WUList.Remove(PartID);
                        System.IO.File.Delete(PartID + ".part");
                        System.IO.File.Delete(PartID + ".part.zip");
                    }
                    else if (oRoot.ChildNodes[i].Attributes["code"].Value == "301")
                    {
                        Logger.AddLogLine("Part" + PartID + " was rejected by the server. Bad size", LoggerLevel.LOG_ALERT);
                        this._WUList.Remove(PartID);
                        System.IO.File.Delete(PartID + ".part");
                        System.IO.File.Delete(PartID + ".part.zip");
                    }
                    else if (oRoot.ChildNodes[i].Attributes["code"].Value == "302")
                    {
                        Logger.AddLogLine("Part" + PartID + " was rejected by the server. Part verification failed.", LoggerLevel.LOG_ALERT);
                        this._WUList.Remove(PartID);
                        System.IO.File.Delete(PartID + ".part");
                        System.IO.File.Delete(PartID + ".part.zip");
                    }
                    else
                    {
                        Logger.AddLogLine(string.Concat(new object[] { "Upload of part ", PartID, " aborted: ", oRoot.ChildNodes[i].Value }), LoggerLevel.LOG_ALERT);
                        WorkUnit WU = this.WUList.GetWUByPartID(PartID);
                        if (WU != null)
                        {
                            WU.EndUploading();
                        }
                    }
                }
                else if ((oRoot.ChildNodes[i].Name == "ok") && (e.Error == null))
                {
                    Logger.AddLogLine("Upload of part " + PartID + " completed", LoggerLevel.LOG_INFO);
                    System.IO.File.Delete(PartID + ".part");
                    System.IO.File.Delete(PartID + ".part.zip");
                    this._WUList.Remove(PartID);
                }
            }
        }

        private void UploadProgressChanged(object sender, UploadProgressChangedEventArgs e)
        {
            uint PartID = (uint) e.UserState;
            long nPercent = Convert.ToInt64((decimal) ((Convert.ToDecimal(e.BytesSent) / Convert.ToDecimal(e.TotalBytesToSend)) * 100));
            WorkUnit WU = this.WUList.GetWUByPartID(PartID);
            if (WU != null)
            {
                WU.PercentUploaded = (int) nPercent;
            }
        }

        public bool AcceptNewParts
        {
            get
            {
                return Settings.AcceptNewParts;
            }
            set
            {
                Settings.AcceptNewParts = value;
            }
        }

        public long CurrentSpeed
        {
            get
            {
                return this._CurrentSpeed;
            }
            set
            {
                if (this._CurrentSpeed != value)
                {
                    this._CurrentSpeed = value;
                    if (this.onCurrentSpeedChanged != null)
                    {
                        this.onCurrentSpeedChanged(this._CurrentSpeed);
                    }
                }
            }
        }

        public bool EnablePublicHashCracking
        {
            get
            {
                return Settings.EnablePublicHashCracking;
            }
            set
            {
                Settings.EnablePublicHashCracking = value;
            }
        }

        public LoggerLevel EventLogThreshold
        {
            get
            {
                return Settings.EventLogThreshold;
            }
            set
            {
                Settings.EventLogThreshold = value;
            }
        }

        public string Hostname
        {
            get
            {
                return Environment.MachineName;
            }
        }

        public bool IsRunning
        {
            get
            {
                return ((this._MainThread != null) && (this._MainThread.ThreadState == System.Threading.ThreadState.Running));
            }
        }

        public int MaxProcessorsToUse
        {
            get
            {
                return this._MaxProcessorsToUse;
            }
            set
            {
                if (this._MaxProcessorsToUse != value)
                {                    
                    Logger.AddLogLine("Setting MaxProcessorsToUse to " + value, LoggerLevel.LOG_DEBUG);
                    Settings.NumCPUToUse = value;
                    WorkThread[] WorkThreadList = new WorkThread[value];
                    int NumRunning = 0;
                    for (int i = 0; i < this._MaxProcessorsToUse; i++)
                    {
                        if (NumRunning < value)
                        {
                            if (this._WorkThreads[i] != null)
                            {
                                WorkThreadList[NumRunning] = this._WorkThreads[i];
                                NumRunning++;
                            }
                        }
                        else
                        {
                            this._WorkThreads[i].Stop();
                        }
                    }
                    this._WorkThreads = WorkThreadList;
                    this._MaxProcessorsToUse = value;

                }
            }
        }

        public int NumProcessors
        {
            get
            {
                return Environment.ProcessorCount;
            }
        }

        public string Password
        {
            get
            {
                return Settings.Password;
            }
            set
            {
                Settings.Password = value;
            }
        }

        public DateTime UploadEndTime
        {
            get
            {
                return Settings.UploadEndTime;
            }
            set
            {
                Settings.UploadEndTime = value;
            }
        }

        public bool UploadParts
        {
            get
            {
                return Settings.UploadParts;
            }
            set
            {
                Settings.UploadParts = value;
            }
        }

        public DateTime UploadStartTime
        {
            get
            {
                return Settings.UploadStartTime;
            }
            set
            {
                Settings.UploadStartTime = value;
            }
        }

        public string Username
        {
            get
            {
                return Settings.Username;
            }
            set
            {
                Settings.Username = value;
            }
        }

        public string Version
        {
            get
            {
                return "1.00";
            }
        }

        public DateTime WorkEndTime
        {
            get
            {
                return Settings.WorkEndTime;
            }
            set
            {
                Settings.WorkEndTime = value;
            }
        }

        public DateTime WorkStartTime
        {
            get
            {
                return Settings.WorkStartTime;
            }
            set
            {
                Settings.WorkStartTime = value;
            }
        }

        public WorkThread[] WorkThreads
        {
            get
            {
                return this._WorkThreads;
            }
        }

        public WorkUnitList WUList
        {
            get
            {
                return this._WUList;
            }
        }

        public delegate void ucCurrentSpeedChanged(long CurrentSpeed);
    }
}

